/*
 * Copyright (C) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Makes a function suitable for use in Array.sort().
 * 
 * @param fn
 *          A function that, given an array element, produces a value to use in
 *          comparisons.
 * @return A function that compares two array elements.
 */
function makeComparer(fn) {
  return function(a, b) {
    var fa = fn(a);
    var fb = fn(b);
    return fa < fb ? -1 : (fa == fb ? 0 : 1);
  };
}

var ESC = 27;
var START_DATE = new Date('April 20, 2010');
var MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
var WAVE_URL_PREFIX = 'https://wave.google.com/wave/waveref/';

//
// State manipulation
//

/**
 * Model object representing a weekly release.
 * 
 * !isReal -> isNew -> isReadyToCreate -> isCreating -> isLinked
 * 
 * @param index
 *          The week index, starting at zero for first week.
 */
function Week(index) {
  this.index = index;
  this.hasState = null;
  this.name = '';
  this.coord = '';
  this.status = '';
  this.waveId = '';

  /**
   * Reads week from gadget state.
   */
  this.fromGadgetState = function() {
    var gadgetState = wave.getState();
    this.hasState = gadgetState.get(this.index + '.hasState', null);
    this.name = gadgetState.get(this.index + '.name', '');
    this.coord = gadgetState.get(this.index + '.coord', '');
    this.status = gadgetState.get(this.index + '.status', '');
    this.waveId = gadgetState.get(this.index + '.waveId', '');
    return this;
  };

  /**
   * Write state of one field for this week to the gadget state.
   * 
   * @param fieldName
   *          The field to write.
   */
  this.toGadgetState = function(fieldName) {
    wave.getState().submitValue(this.index + '.' + fieldName, this[fieldName]);
  };

  /** Causes week to be created in gadget state. */
  this.store = function() {
    this.hasState = 'yes';
    this.toGadgetState('hasState');
  };

  /** Whether the week is real. */
  this.isReal = function() {
    return this.hasState != null;
  };

  /** Whether the week is new. */
  this.isNew = function() {
    return this.coord == '' || this.name == '';
  };

  /** Whether the week is ready for a wave to be created. */
  this.isReadyToCreate = function() {
    return !this.isNew() && this.status == '' && this.waveId == '';
  };

  /** Whether the corresponding release wave has been created. */
  this.isReleaseWaveCreated = function() {
    return this.waveId != '';
  };

  /** Whether the week is linked, and therefore mostly frozen. */
  this.isLinked = function() {
    return this.waveId != '';
  };
}

/**
 * Gets the entire state of all release.
 * 
 * @return An array of Weeks.
 */
function getAllState() {
  var result = [];
  var next = new Week(0).fromGadgetState();
  while (next.isReal()) {
    result.push(next);
    next = new Week(result.length).fromGadgetState();
  }
  return result;
}

/**
 * Writes the state.
 * 
 * @param index
 *          The week to write.
 * @param field
 *          The name of the field to write.
 * @param value
 *          The value to write.
 */
function putState(index, field, value) {
  var week = new Week(index).fromGadgetState();
  week[field] = value;
  week.toGadgetState(field);
}

//
// UI manipulation
//

/**
 * Sets a JQuery data item on the given element's row.
 * 
 * @param element
 *          An element that is a row, or a child of a row.
 * @param index
 *          The data to set.
 */
function setIndex(element, index) {
  element.closest('li.row').data('index', index);
}

/**
 * Gets the jquery data item on the given elements row.
 * 
 * @param element
 *          An element that is a row, or a child of a row.
 * @return The data previously set with setIndex().
 */
function getIndex(element) {
  return element.closest('li.row').data('index');
}

/**
 * Gets the actual date corresponding to the week index.
 * 
 * @param index
 *          The week index.
 * @return A Date.
 */
function getDateForIndex(index) {
  return new Date(START_DATE.getTime() + 7 * MILLIS_PER_DAY * index);
}

/**
 * Formats a date for display.
 * 
 * @param dt
 *          a Date
 * @return a String
 */
function formatDate(dt) {
  var MONTHS = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep',
      'oct', 'nov', 'dec' ];
  return dt.getFullYear() + '-' + MONTHS[dt.getMonth()] + '-' + dt.getDate();
}

/**
 * Appends a row to the on screen list.
 */
function addUiRow() {
  var lastRow = $('.stuff .row:last-child');
  var newIndex = $('.stuff .row.val').size();
  var newRow = $('#rowTemplate > .row.val').clone();
  lastRow.after(newRow);
  newRow.find('div.cell.date .content').append(
      formatDate(getDateForIndex(newIndex)));
  setIndex(newRow, newIndex);
}

/**
 * Updates the coordinator name select.
 * 
 * @param select
 *          The select element.
 * @param coordId
 *          participantId of the coordinator.
 */
function updateCoordinator(select, coordId) {
  function makeOption(participantId, selected) {
    var result = '<option ';
    if (selected) {
      result += 'selected=selected ';
    }
    result += 'value=\'' + gadgets.util.escapeString(participantId) + '\'>';
    if (participantId == '') {
      result += 'No one';
    } else {
      var participant = wave.getParticipantById(participantId);
      if (participant == null) {
        result += gadgets.util.escapeString(participantId);
      } else {
        result += gadgets.util.escapeString(participant.getDisplayName());
      }
    }
    result += '</option>';
    return result;
  }

  // Set the currently selected participant
  select.empty();
  select.append(makeOption(coordId, true));

  // Add everyone on the wave, in alphabetical order, except the one already set
  var participants = wave.getParticipants();
  participants.sort(makeComparer(function(p) {
    return p.getDisplayName();
  }));

  for ( var i = 0; i < participants.length; i++) {
    var pId = participants[i].getId();
    if (coordId != pId && !/gwave.com$/.test(pId)) {
      select.append(makeOption(pId, false));
    }
  }
}

/**
 * Updates a row on the UI with a given week
 * 
 * @param week
 *          The week to display.
 */
function updateUiRow(week) {
  wave.log('updateUiRow(' + week + ')');
  var existingRows = '.stuff li.row.val';

  // Ensure row exists
  if ($(existingRows).size() == 0) {
    addUiRow();
  }
  while ($(existingRows).size() <= week.index) {
    addUiRow();
  }

  // Set its values
  var newRow = $(existingRows).eq(week.index);
  var escapedName = gadgets.util.escapeString(week.name);
  nameCell = newRow.find('div.cell.name');
  if (week.waveId == '') {
    if (!nameCell.hasClass('focused')) {
      // do not overwrite if user is currently editing
      nameCell.find('input').val(escapedName);
    }
  } else {
    nameCell.html('<a href=\'javascript:\'>' + escapedName + '</a>');
    var link = nameCell.find('a');
    var href = WAVE_URL_PREFIX + week.waveId.replace(/\+/, '%252B')
    link.click(function(e) {
      e.preventDefault();
      top.location.replace(href);
    });
  }

  updateCoordinator(newRow.find('div.cell.coord select'), week.coord);

  var statusCell = newRow.find('div.cell.status');
  if (week.isReadyToCreate()) {
    statusCell.find('button').removeClass('noshow');
  } else {
    statusCell.find('button').addClass('noshow');
  }
  if (week.status != '') {
    statusCell.find('span').html(gadgets.util.escapeString(week.status));
  }

  // Set coordinator picture, if available
  var participant = wave.getParticipantById(week.coord);
  if (participant != null) {
    var img = newRow.find('div.cell.coord .thumbnail img');
    img.attr('src', participant.getThumbnailUrl());
    img.removeClass('noshow');
  }
}

//
// Event handling
//

/**
 * Handles wave having been updated - either participants or wave.
 */
function onWaveUpdated() {
  // TODO(alan): separate handling of participant and wave state updates.
  if (wave.getState() == null || wave.getParticipants().length == 0) {
    // Get out if gadget not yet fully initialised
    wave.log('wave state: ' + wave.getState());
    wave.log('participants: ' + wave.getParticipants().length);
    return;
  }
  var weeks = getAllState();
  for ( var i = 0; i < weeks.length; i++) {
    updateUiRow(weeks[i]);
  }
  gadgets.window.adjustHeight();
}

/**
 * Handles key being pressed on an input element.
 * 
 * @param event
 *          The key press event.
 * @param field
 *          name of the state field corresponding to the input element.
 */
function onRowInputKey(event, field) {
  if (event.keyCode == ESC) {
    var element = $(event.target);
    element.val(new Week(getIndex(element)).fromGadgetState()[field]);
  }
}

/**
 * Gadget initialisation function.
 */
function init() {
  // Set up input fields to act on focus and blur
  $('input:text').live('focus', function(event) {
    $(this).addClass('focused');
  });
  $('input:text').live('blur', function(event) {
    $(this).removeClass('focused');
  });

  // Capture edits and escape key
  $('div.cell.name input:text').live('blur', function() {
    putState(getIndex($(this)), 'name', $(this).val());
  });
  $('div.cell.name input:text').live('keydown', function(event) {
    onRowInputKey(event, 'name');
  });
  $('div.cell.coord select').live(
      'change',
      function() {
        putState(getIndex($(this)), 'coord', $(this).children(':selected')
            .first().val());
      })
  $('div.cell.status button').live('click', function() {
    // Some change to kick the robot into action.
      putState(getIndex($(this)), 'status', 'Creating Release Wave');
    });

  // Set up add row button
  $('#addRow').click(function(event) {
    // Add in a blank state
      var newIndex = getAllState().length;
      new Week(newIndex).store();

      // TODO(alan):set focus on new release, disable button until last state is
      // not blank
    });

  wave.setParticipantCallback(onWaveUpdated);
  wave.setStateCallback(onWaveUpdated);
}

gadgets.util.registerOnLoadHandler(init);
